/*
 * Copyright 2011 J. Auer, J. Andert, F.-J. Jendrusch, U. Celebi
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.fuberlin.replication.communication.wifi.java.net;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Vector;

import de.fuberlin.replication.communication.Communication;
import de.fuberlin.replication.communication.CommunicationNotYetEstablishedException;

/**
 * Support for sending/receiving data and discovery of remote devices in an wireless network, using TCP/IP.
 */
public class JavaNetWiFiCommunication implements Communication {

	private static final String MULTICASTADDRESS = "230.3.20.11";
	private static final int PORT = 31214;

	private Socket socket;
	private ObjectInputStream inputStream;
	private ObjectOutputStream outputStream;

	@Override
	public Object read() throws IOException {
		if (inputStream == null)
			throw new CommunicationNotYetEstablishedException("Communication not opened");

		try {
			return inputStream.readObject();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (NoClassDefFoundError e) {
			e.printStackTrace();
		}

		return null;
	}

	@Override
	public void write(Object object) throws IOException {
		if (outputStream == null)
			throw new CommunicationNotYetEstablishedException("Communication not opened");

		outputStream.writeObject(object);
		outputStream.flush();
	}

	private void open() throws IOException {
		print("connected");
		outputStream = new ObjectOutputStream(socket.getOutputStream());
		inputStream = new ObjectInputStream(socket.getInputStream());
	}

	/**
	 * Actively connect to a remote device. The remote device must have called <code>listen</code> before.
	 * 
	 * @param host Remote host to connect to
	 * @param timeout Time in ms to try. 0 for unlimited
	 * @throws UnknownHostException Thrown, if host is not found
	 * @throws IOException Thrown, if some Exception occurs at the underlying socket
	 * @see #listen(int)
	 */
	public void connect(String host, int timeout) throws IOException {
		print("connect to: " + host);
		long haltTime = System.currentTimeMillis() + timeout;

		while (timeout == 0 || System.currentTimeMillis() < haltTime)
			try {
				socket = new Socket(InetAddress.getByName(host), PORT);

				if (socket != null)
					break;
			} catch (IOException e) {
				e.printStackTrace();

				try {
					wait(1000);
				} catch (InterruptedException e1) {
				}
			}
		open();
	}

	/**
	 * Wait for incoming connections.
	 * 
	 * @param timeout Time in ms to try
	 * @throws IOException Thrown, if some Exception occurs at the underlying socket
	 * @see #connect(String, int)
	 */
	public void listen(int timeout) throws IOException {
		ServerSocket s = new ServerSocket(PORT);
		s.setSoTimeout(timeout);
		print("listening");

		try {
			socket = s.accept();
			open();
		} finally {
			s.close();
		}
	}

	/**
	 * Auto decide socket direction.
	 * 
	 * @param localHost Local IP address
	 * @param remoteHost Remote IP address
	 * @throws IOException Thrown, if some Exception occurs at the underlying socket
	 */
	public void autoConnect(String localHost, String remoteHost) throws IOException {
		if (localHost.compareTo(remoteHost) < 0)
			listen(0);
		else
			connect(remoteHost, 0);

		if (socket == null)
			throw new IOException("Unable to connect to host");
	}

	/**
	 * Close the communication socket.
	 */
	public void close() {
		try {
			inputStream.close();
			outputStream.close();
			socket.close();
		} catch (NullPointerException e) {
		} catch (IOException e) {
		}
	}

	/**
	 * Auxiliary class for broadcasting "hello-packages" for a given time.
	 */
	protected static class Announcer implements Runnable {

		private MulticastSocket mcSocket;
		private DatagramPacket packet;
		private int iterations;

		/**
		 * Constructor.
		 * 
		 * @param socket MulticastSocket to send packages over
		 * @param timeout Time in ms. The class will broadcast for timeout ms.
		 * @throws UnknownHostException Thrown, if MulticastGroup is not known anymore
		 */
		public Announcer(MulticastSocket socket, int timeout) throws UnknownHostException {
			this.mcSocket = socket;
			packet = new DatagramPacket("sync?".getBytes(), 5, InetAddress.getByName(MULTICASTADDRESS), PORT);
			iterations = timeout / 1000;
		}

		@Override
		public void run() {
			for (int i = 0; i < iterations; ++i) {
				try {
					mcSocket.send(packet);
				} catch (IOException e) {
					e.printStackTrace();
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * Discover devices in the local network, this device is connected to, that are discovering, too. May fail, if the
	 * network don't support broadcasts.
	 * 
	 * @param timeout Time to search, in ms.
	 * @return Array of strings, representing the IP addresses of discovered devices
	 * @throws IOException Thrown, of some error occurs on the underlying socket
	 */
	public static String[] discoverDevices(int timeout) throws IOException {
		// Start a thread, that sends out "hello-packages"
		print("collecting device data");
		MulticastSocket socket = new MulticastSocket(PORT);
		socket.setTimeToLive(2);
		socket.joinGroup(InetAddress.getByName(MULTICASTADDRESS));
		new Thread(new Announcer(socket, timeout)).start();

		// Receive incoming packages for a given time, and collect the remote addresses in a list
		Vector<String> devices = new Vector<String>();
		long haltTime = System.currentTimeMillis() + timeout;

		while (System.currentTimeMillis() < haltTime) {
			DatagramPacket d = new DatagramPacket(new byte[5], 5);
			socket.receive(d);
			String remoteHost = d.getAddress().getHostName();

			if (new String(d.getData()).equals("sync?") && !devices.contains(remoteHost)) {
				devices.add(remoteHost);
				print("device found: " + remoteHost);
			}
		}

		socket.close();
		String[] result = new String[devices.size()];
		devices.toArray(result);

		return result;
	}

	private static void print(String msg) {
		System.out.println("WiFiCom: " + msg);
	}
}